查看原文
其他

【第1304期】聊一聊Redux的前身Flux

Gloria 前端早读课 2019-06-26

前言

抽奖这东西,欢喜就好,祝各位端午节安康。今日早读文章由网易考拉海购@Gloria投稿分享。

@Gloria,前端工程师,就职于网易考拉海购

正文从这开始~

现在诸多状态管理方案涌现,每种方案的背后都有支撑其实现的思想,而这些思想并不是“空穴来风”,都是为了解决开发中出现的各种问题而诞生。

接下来的会深入探讨时下比较流行的两种状态管理方案,Redux,Mobx。

为了深入了解Redux,不可避免地就要谈到它的前身Flux。

概念

在正文开始之前,我们需要理解在平时使用诸如react.jsvue.js这类MV*框架时接触到Model和View的概念。

MVVM两个概念示意图.PNG-3.2kB

一个完整的交互流程就如上图所示。

View

View,意为“视图”,即最终在浏览器上看到的页面元素。

Model

Model,翻译过来就是“模型”,那…什么是“模型”呢?且看下面这些代码。

<div>
   
<p>{{a.b}}</p>
   
<p>{{a.c}}</p>
   
<p>{{a.d}}</p>
</div>

上面这一段代码,其中a.ba.ca.d,每当这些属性值发生改变之后,框架会帮助我们生成View。

如果我们再稍微宏观地看待这一问题,其实可以将a这个对象看作是data(数据),而上面的html代码就是template(模板),于是就有了这种理解:框架通过将data应用到template上,最后生成View,即b过程。

在这里data+template就是Model,即所谓的“模型”,而通常意义上template是固定不变的,不会动态发生变化(这种动态变化已经被涵盖在模板本身的语法中了),所以大多数时候我们实现的各种交互就是改变data上属性的过程,示意图中的a过程。

目前开发中存在的问题

ok,介绍完Model和View这两个概念后,在这两个抽象层面上谈一谈平时开发过程中遇到的问题。

碎片化修改

我们实现交互基础就是操作Model,就拿上面那个代码片段来说,操作Model就是修改a.ba.ca.d,于是操作这个Model就会像下图所展示的情况一样,修改操作会“碎片化”地存在于整个组件文件的各个角落。

碎片化.PNG-24.6kB

对于没有严格开发模式限制的工程,一旦页面复杂度上去了,如果多人维护这样的代码,添加feature的时候可以说会比较刺激了。

大多数情况的表情应该是这样的

黑人问号.jpg-5.4kB
数据流捉摸不定

1. 复杂的数据流

先来谈一谈vue.js之类基于检测数据变动实现局部更新的MVVM框架,这些框架提供了多种多样影响Model的方式。

看一看这张图

复杂数据流.PNG-13.7kB

最明显的,跟上面那张图相比,增加了从View到Model这一个方向,这种改变自然是框架“双向数据绑定”所带来的。毫无疑问,这种feature给我们带来了一定的便利,但与此同时,它会使得最终生成View的逻辑更加扑朔迷离,为什么这么说呢?

从另外一个角度看待这个问题,最终到View的不同路径数越多,就代表生成View的方式越多,生成View的方式越多,代码的可预测性就越弱。

很显然,在这张图当中,以View做为终点的路径还是不少的,以碎片化修改为起点的路径有2条,以View作为起点的路径有3条。

从路径数量这个角度,很直观地就可以得出这类框架设计对于代码可维护性是不友好的。

2. 简单的数据流

但是,也有一些框架数据流是比较简单的(比如React),改变Model的方式仅限于手动调用setState,或者View触发setState,在代码的predicatable(可预测性)方面有比较大的优势。

React数据流.PNG-12.3kB

OK,以上这些与这次的主题有什么关系呢?

Flux

上面已经谈到了现在MV*框架中存在的问题,比如vue,react等都仅仅是视图层框架,也就是说,它们只负责渲染View,而对于Model的变化没有统一的管理方案。

Flux的出现其实就是为了管理Model的变化,使得应用的可伸缩性,和代码的可预测性更强。

单向数据流基础

Flux其实就是在React单向数据流的基础上做了一层对Model的管理,那就看一看它是如何借鉴的。

单向数据流基础.PNG-17.9kB

相比其它框架设计,最大的不同之处就是:React没有View-->Model这个方向。就拿上面复杂数据流方案来说,以View为起点的数据流路径就可以减少两条,保证了最终生成View的逻辑是相对清晰的。

如何看待Flux架构

Flux其实提供了一整套Model修改模式。这种模式的初衷,在我看来,就是为了提高代码的可预测性,再通俗一点就是,当你看到了一段代码时,让你更清晰地知道它会做什么。

为什么这么说呢?我们在维护工程时无外乎就是扮演两个角色:使用者和定义者。而往往我们在代码中确很少体现这两种角色抽象,最多也只是在文档和代码规范层面,任你玩出花来,也很难做到比较高的通用性。

再具体一点,Flux将使用者和定义者的抽象引入了Model的修改过程。类似Clent-Service架构,如果使用者(客户端)想要修改数据库,必须通过调用定义者(服务端)提供的接口实现。

1. 请求

在Flux中,request(请求)等价于action,触发一个action相当调用一次接口,action的type字段相当于接口地址,其它字段相当于payLoad(请求参数)。

action应该是一个对象:

{
   type
: 'delete-todo',   //接口地址
   todoID
: '1234'      //payLoad
};

既然将action当作了request,那么我们应该如何实现server(服务器)呢?

2. 路由

就像Clent-Service中一样,server接收请求并将不同的请求映射为相应的数据库修改操作。将server中接收请求的部分称为router(路由)。

一个router应该长这样:

let router = (function router(){
   let dataBase
= {todos: []}; //模拟数据库的对象
   
return function(request){
       
switch(request.type){
           
case 'ADD_TODO': deleteToDo(request, dataBase); break;
           
...
       
}
   
};
})();

发送一个请求:

router({type: 'delete-todo', todoID: '1234'});

deleteToDo()其实就是相应修改数据库的操作,里面的具体逻辑需要我们自己写,显然,删除一个”待办事项”,deleteToDo()应该长下面这样:

function deleteToDo(request, dataBase){
   let todos
= dataBase.todos;
   
for(let i = 0; i < todos; i++){
       
if(todos[i].id === request.todoID){
           todos
.splice(i, 1);
           
return;
       
}
   
}
}

ok,到目前为止,整个流程已经跑通了。定义一个request,使用router发送这个request,router根据request地址分配相应的数据库处理逻辑,于是就得到了下面这种抽象:

单dataBase架构.PNG-7kB

用上面这种架构已经可以勉强驾驭一些比较简单的应用场景,而面对稍微复杂一点的应用场景就捉襟见肘了,为什么这么说呢?

这种架构最基本的应用单元就是组件,每个组件的Model其实就是对应的dataBase,如果我们想在某个组件内修改其它组件的dataBase,就需要拿到这个组件的router,而”拿router”这件事可并没有那么简单。。大体上根据组件之间的关系,分为3种情况:父子关系、爷孙关系和兄弟关系,于是就会出现下面这种情况。

多dataBase架构.PNG-23.1kB

为了解决这一问题,Flux的另一个概念就来了,dispatcher。

3. 请求分发器

Flux的dispatcher(请求分发器),其实解决了上述问题。

dispatcher相对各个组件而言是全局性的,它可以将请求发送到所有的router,用户无需知道他需要请求的router,让每个router自行处理进来的request,这种抽象其实是将request视为全局性请求,一个request可以同时操作多个dataBase。

引入dispatcher.PNG-16.7kB

当然,dispatcher不会自己寻找它需要分发到的router,我们需要调用register()方法手动注册router

dispatcher.register(router);

在注册好router后,直接调用dispatcher的dispatch()方法即可,可以像下面这样发送一个request:

dispatcher.dispatch({type: 'delete-todo', todoID: '1234'});

默认情况下,Flux会按照注册顺序依次将request放进router。如果我们希望自定义发送request后,部分router的执行顺序怎么办?Flux提供了waitFor()方法。

举个例子:routerA接收到请求之后,希望依次经过routerB,和routerC,可以像下面伪代码这样实现:

let tokenB = dispatcher.register(routerB);
let tokenC
= dispatcher.register(routerC);
let routerA
= function(request){
   
switch(request.type){
       
case 'ADD_TODO': dispatcher.waitFor(tokenB, tokenC); break;
       
...
   
}
};

OK,你必须提前拿到routerB和routerC的token,然后按照顺序传入waitFor()方法(个人认为这种”拿token”,无异于上面提到的”拿router”,是一个设计缺陷)。

4. 数据库

dataBase(数据库)其实就代表了组件的state(状态)。

而Flux将router和dataBase视为一体,将请求的解析和数据库的修改统一交给store来处理。

store.reduce()相当于router,而store._state则相当于dataBase,于是就有了下面这种架构

store架构.PNG-19kB

最后,Flux采用了向外抛事件的方式,将_state映射到Model的工作交给用户去解决。

你可以调用store.addListener()方法,传入回调函数即可监听到_state的变化。

store.addListener(() => {
   let state
= store.getState();
   
...映射到Model的操作...
});

结语

Flux的一整套抽象(action,dispatcher,store),在单向数据流的基础上可以提高应用的可维护性和代码的可预测性。然而,全局action+多store的架构面对复杂的应用依然不能很好地解决复杂数据流的问题,waitFor()虽然可以满足自定义多store接收action的顺序,但是它会让数据流变得复杂,难以维护。

Redux作为Flux的继承者,单store的架构其实就很好地避免了上述问题,之后的文章会深入分析Redux是如何在Flux的基础上改进自身架构的。

参考:

Flux官方介绍:In Depth Overview

Flux官方仓库:github.com/facebook/flux

关于本文

作者:@Gloria

原文:

https://zhuanlan.zhihu.com/p/38050036

最后,为你推荐


【第1265期】那些前端MVVM框架是如何诞生的

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存